/*
 * #%L
 * java model
 * %%
 * Copyright (C) 2009 - 2011 prefabware.com Stefan Isele
 * %%
 * All Rights Reserved.
 * This program is dual-licensed under the GNU General Public license
 * or a commercial license.
 * You may select to use one or the other of these licenses.
 * To obtain a commercial license please contact Stefan Isele at www.stefan-isele.de
 * Any modifications to this file must keep this entire header intact.
 * Redistribution, except as permitted by the selected license, is prohibited.
 * #L%
 */
package com.prefabware.jmodel;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import com.prefabware.commons.CollectionUtil;
import com.prefabware.commons.Condition;
import com.prefabware.commons.NamespaceImpl;
import com.prefabware.commons.collection.Filter;
import com.prefabware.commons.collection.Gettable;
import com.prefabware.commons.file.RelativePath;
import com.prefabware.commons.file.SourceFile;
import com.prefabware.commons.file.SourceFolder;
import com.prefabware.jmodel.expression.JExpression;
import com.prefabware.jmodel.expression.JVariableBase;
import com.prefabware.jmodel.expression.JVariableDeclaration;
import com.prefabware.jmodel.expression.JVariableDeclarationBase;
import com.prefabware.jmodel.expression.literal.JLiteral;
import com.prefabware.jmodel.javadoc.JMethodJavadoc;
import com.prefabware.jmodel.typefactory.TypeFactory;

public class JTypeUtil {
	public static final String SERIAL_VERSION_UID = "serialVersionUID";
	public static final Filter<JMember> FILTER_INCLUDING_ALL_MEMBER = new Filter<JMember>() {

		@Override
		public boolean includes(JMember value) {
			return true;
		}
	};
	public static final Filter<JMember> FILTER_INCLUDING_FIELDS_ONLY = new Filter<JMember>() {

		@Override
		public boolean includes(JMember value) {
			return value instanceof JField;
		}
	};
	public static final Filter<JField> FILTER_EXCLUDE_SERIAL_VERSION_UID = new Filter<JField>() {

		@Override
		public boolean includes(JField value) {
			return !value.getName().equals(SERIAL_VERSION_UID);
		}
	};

	/**
	 * adds a zero-argument constructor without modifiers
	 * 
	 * @param cls
	 *            the class the constructor will be added to
	 * @param modifiers 
	 * @param visibility
	 *            the visibility eg. public
	 * @return
	 */
	public static JConstructor addDefaultConstructor(JClass cls,
			JModifiers modifiers, JVisibility visibility) {
		// add an default-constructor
		JConstructor defaultConstructor = cls.getDefaultConstructor();
		if (defaultConstructor == null) {
			JConstructorDeclaration dec = new JConstructorDeclaration(cls.getQualifiedName().append(cls.getQualifiedName().getSimpleName()), cls,
					modifiers, visibility, null);
			JConstructor constructor = new JConstructor(dec, cls);
			cls.add(constructor);
			return constructor;
		} else {
			return defaultConstructor;
		}
	}

	private static void addParams(JMethodJavadoc javadoc, JMethod method) {
		Gettable<String, JArgument> getArgumentName = new Gettable<String, JArgument>() {

			@Override
			public String get(JArgument from) {
				return from.getName();
			}
		};
		;
		;
		javadoc.setParams(CollectionUtil.getList(getArgumentName,
				method.getArguments()));
	}

	private static void addReturn(JMethodJavadoc javadoc, JType returnType) {
		// no @return for void-methods
		if (!returnType.getQualifiedName().getName()
				.equals(void.class.getName())) {
			javadoc.setReturn(returnType.getName());
		}
	};

	private static void addThrows(JMethodJavadoc javadoc, JMethod method) {
		Gettable<String, JThrowable> getExceptionName = new Gettable<String, JThrowable>() {

			@Override
			public String get(JThrowable from) {
				return from.getQualifiedName().getName();
			}
		};
		javadoc.setThrows(CollectionUtil.getList(getExceptionName,
				method.getThrows()));
	}

	public static JMethodJavadoc getDefaultJavadoc(JConstructor constructor) {
		JMethodJavadoc javadoc = new JMethodJavadoc();
		// @return is allowed also for constrctors, but usually not used
		addThrows(javadoc, constructor);
		addParams(javadoc, constructor);
		return javadoc;
	}

	public static JMethodJavadoc getDefaultJavadoc(JMethod method) {
		JMethodJavadoc javadoc = new JMethodJavadoc();
		JType returnType = method.getDeclaration().getType();
		addReturn(javadoc, returnType);
		addThrows(javadoc, method);
		addParams(javadoc, method);
		return javadoc;
	}

	public static boolean isBoolean(JType type) {
		return (type.getQualifiedName().getName()
				.equals(Boolean.class.getName()) || type.getQualifiedName()
				.getName().equals(boolean.class.getName()));
	}

	/**
	 * creates a new declaration using type and is array of the given
	 * declaration
	 * 
	 * @param name
	 * @param declaringType TODO
	 * @param modifiers
	 * @param visibility
	 * @param field
	 * @return
	 */
	public static JFieldDeclaration newFieldDeclaration(String name, JType type,
			JType declaringType, JModifiers modifiers,
			JVisibility visibility, JType... typeArguments) {
		Condition.notNull(declaringType, "..declaringType");
		JFieldDeclaration fieldDeclaration = new JFieldDeclaration(declaringType.getQualifiedName().append(name), type, modifiers, visibility,
						asList(typeArguments));
		fieldDeclaration.setDeclaringType(declaringType);
		return fieldDeclaration;
	}
	public static JMethodDeclaration newMethodDeclaration(String name, JType type,
			JType declaringType, JModifiers modifiers,
			JVisibility visibility, JType... typeArguments) {
		Condition.notNull(declaringType, "..declaringType");
		JMethodDeclaration methodDeclaration = new JMethodDeclaration(declaringType.getQualifiedName().append(name), type, modifiers, visibility,
						asList(typeArguments));
		methodDeclaration.setDeclaringType(declaringType);
		return methodDeclaration;
	}

	public static List<JType> asList(JType... typeArguments) {
		if (typeArguments == null) {
			return Collections.emptyList();
		} else {
			return Arrays.asList(typeArguments);
		}
	}
	public static JType[] toArray(List<JType>types) {
		if (types == null) {
			return null;
		} else {
			return types.toArray(new JType[0]);
		}
	}

	public static SourceFile newJavaSourceFile(SourceFolder folder, JType type) {
		SourceFolder subFolder = folder.append(new RelativePath(
				new NamespaceImpl(type.getQualifiedName().getQualifier(),
						NamespaceImpl.SEPARATOR_PACKAGE).pathStyle()));
		return new SourceFile(subFolder, type.getName() + ".java");
	}

	public static JField newJField(String name, JTypeRef typeRef,
			JType declaringType, JModifiers modifiers,
			JVisibility visibility) {
		return newJField(name,typeRef.getType(),declaringType,modifiers,visibility,typeRef.getTypeArguments().toArray(new JType[0]));
	}
	/**
	 * creates a new instance of JField.
	 * 
	 * @param name
	 * @param type
	 *            the type of the field
	 * @param declaringType 
	 * @param visibility
	 * @param typeArguments 
	 * @param variableDeclaration
	 * @return a new instance of JField
	 */
	public static JField newJField(String name, JType type,
			JType declaringType, JModifiers modifiers,
			JVisibility visibility, JType... typeArguments) {
		JFieldDeclaration vd = newFieldDeclaration(name, type, declaringType,
				modifiers, visibility, typeArguments);
		return new JField(vd);
	}

	public static JEnumerationValue newJEnumerationConstant(String name,
			JType type, JModifiers modifiers, JVisibility visibility,
			JType... typeArguments) {
		JFieldDeclaration vd = newFieldDeclaration(name, type, type,
				modifiers, visibility, typeArguments);
		return new JEnumerationValue(vd);
	}
	public static JMethod newJMethod(String name, JTypeRef typeRef,
			JType declaringType, JModifiers modifiers,
			JVisibility visibility) {
		return newJMethod(name,typeRef.getType(),declaringType,modifiers,visibility,typeRef.getTypeArguments().toArray(new JType[0]));
	}
	/**
	 * creates a new method
	 * does NOT add it to the declaringType
	 * callers have to do this, after !! the signature is complete e.g all arguments are added !
	 * because this changes the hashcode of the signature. 
	 * @param name
	 * @param returnType
	 * @param declaringType
	 * @param modifiers
	 * @param visibility
	 * @param typeArguments
	 * @return
	 */
	public static JMethod newJMethod(String name, JType returnType,
			JType declaringType, JModifiers modifiers,
			JVisibility visibility, JType... typeArguments) {
		Condition.notNull(declaringType, "..declaringType");
		JMethodDeclaration vd = JTypeUtil.newMethodDeclaration(name, returnType,
				declaringType, modifiers, visibility, typeArguments);
		JMethod method = new JMethod(vd);
		return method;
	}

	public static JPackage newJPackage(String string) {
		return new JPackage(string);
	}

	/**
	 * @param name
	 * @param type
	 * @param isFinal TODO
	 * @param typeArguments 
	 * @param method 
	 * @return a new variable of the given name and type, without modifiers
	 */
	public static JVariableBase newJVariable(String name, JType type,JMethodDeclaration method,
			boolean isFinal, JType... typeArguments ) {
		JVariableDeclaration dcl=new JVariableDeclarationBase(isFinal, name, type, asList(typeArguments));
		return new JVariableBase(dcl);
	}

	public static JArgument newParameter(boolean isFinal,String name, JTypeRef typeRef,
			 JMethodDeclaration methodDeclaration) {
		return newParameter(isFinal,name,typeRef.getType(),methodDeclaration,typeRef.getTypeArguments().toArray(new JType[0]));
	}
	/**
	 * creates a parameter of the given name associated with the given field
	 * @param isFinal TODO
	 * @param name
	 *            of the parameter
	 * @param type 
	 * @param methodDeclaration TODO
	 * @param typeArgument 
	 * @param field
	 *            that will be associated with the parameter. the parameter will
	 *            have the same type and array
	 * 
	 * @return
	 */
	public static JArgument newParameter(boolean isFinal, String name,
			JType type, JMethodDeclaration methodDeclaration, JType... typeArgument) {
		JVariableDeclaration dcl = new JVariableDeclarationBase(isFinal,name,type,asList(typeArgument));
		JVariableBase v=new JVariableBase(dcl);
		return new JArgument(v);
	}

	/**
	 * creates a new field without modifiers and private visibility which is the
	 * type of field mostly used
	 * 
	 * @param name
	 * @param type
	 * @param declaringType TODO
	 * @param typeArguments 
	 * @return
	 */
	public static JField newPrivateJField(String name, JType type,
			JType declaringType, JType... typeArguments) {
		JFieldDeclaration vd = newFieldDeclaration(name, type, declaringType,
				JModifiers.NONE, JVisibility.PRIVATE, typeArguments);
		return new JField(vd);
	}

	/**
	 * makes the given class serializable
	 * by
	 * implementing the interface java.io.Serializable
	 * and adding the declaration for field
	 * private static final long serialVersionUID
	 * using the initilaizer. A Initializer may be an {@link JLiteral}
	 * example :
	 * for <br>
	 * private static final long serialVersionUID <br>
	 * initializer= JLiteralBase.create(1L)
	 * @param jClass
	 * @param typeFactory
	 * @param initializer an expression to initialize the serialVersionUID e.g : JLiteralBase.create(1L)
	 * @return
	 */
	public static JClass makeSerializable(JClass jClass,
			TypeFactory typeFactory, JExpression initializer) {
		Condition.notNull(initializer, "..initializer");
		JInterface serializable_ = typeFactory.getJType(Serializable.class);
		jClass.add(serializable_);
		JField serialUID = newJField(SERIAL_VERSION_UID,
				typeFactory.getJType(long.class),jClass,JModifiers.FINAL_STATIC,JVisibility.PRIVATE );
		serialUID.setInitializer(initializer);
		jClass.add(serialUID);
		return jClass;
	}
}
